Passed
Branch wavefile-rw (6b0ae0)
by Rafael S.
02:21
created

WaveFile.assureUncompressed_   A

Complexity

Conditions 4

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from 'bitdepth';
33
import * as imaadpcm from 'imaadpcm';
34
import * as alawmulaw from 'alawmulaw';
35
import {encode, decode} from 'base64-arraybuffer-es6';
36
import WaveFileCreator from './lib/wavefile-creator';
37
import truncateSamples from './lib/truncate-samples';
38
import fixRIFFTag from './lib/fix-riff-tag';
39
import {unpackArray, unpackArrayTo, unpack, packTo} from 'byte-data';
40
41
/**
42
 * A class to manipulate wav files.
43
 */
44
export default class WaveFile extends WaveFileCreator {
45
46
  /**
47
   * Force a file as RIFF.
48
   */
49
  toRIFF() {
50
    this.fromScratch(
51
      this.fmt.numChannels,
52
      this.fmt.sampleRate,
53
      this.bitDepth,
54
      unpackArray(this.data.samples, this.dataType));
55
  }
56
57
  /**
58
   * Force a file as RIFX.
59
   */
60
  toRIFX() {
61
    this.fromScratch(
62
      this.fmt.numChannels,
63
      this.fmt.sampleRate,
64
      this.bitDepth,
65
      unpackArray(this.data.samples, this.dataType),
66
      {container: 'RIFX'});
67
  }
68
69
  /**
70
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
71
   * @throws {Error} If sample rate is not 8000.
72
   * @throws {Error} If number of channels is not 1.
73
   */
74
  toIMAADPCM() {
75
    if (this.fmt.sampleRate !== 8000) {
76
      throw new Error(
77
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
78
    } else if (this.fmt.numChannels !== 1) {
79
      throw new Error(
80
        'Only mono files can be compressed as IMA-ADPCM.');
81
    } else {
82
      this.assure16Bit_();
83
      /** @type {!Int16Array} */
84
      let output = new Int16Array(this.data.samples.length / 2);
85
      unpackArrayTo(this.data.samples, this.dataType, output);
86
      this.fromScratch(
87
        this.fmt.numChannels,
88
        this.fmt.sampleRate,
89
        '4',
90
        imaadpcm.encode(output),
91
        {container: this.correctContainer_()});
92
    }
93
  }
94
95
  /**
96
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
97
   * @param {string} bitDepthCode The new bit depth of the samples.
98
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
99
   *    Optional. Default is 16.
100
   */
101
  fromIMAADPCM(bitDepthCode='16') {
102
    this.fromScratch(
103
      this.fmt.numChannels,
104
      this.fmt.sampleRate,
105
      '16',
106
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
107
      {container: this.correctContainer_()});
108
    if (bitDepthCode != '16') {
109
      this.toBitDepth(bitDepthCode);
110
    }
111
  }
112
113
  /**
114
   * Encode a 16-bit wave file as 8-bit A-Law.
115
   */
116
  toALaw() {
117
    this.assure16Bit_();
118
    /** @type {!Int16Array} */
119
    let output = new Int16Array(this.data.samples.length / 2);
120
    unpackArrayTo(this.data.samples, this.dataType, output);
121
    this.fromScratch(
122
      this.fmt.numChannels,
123
      this.fmt.sampleRate,
124
      '8a',
125
      alawmulaw.alaw.encode(output),
126
      {container: this.correctContainer_()});
127
  }
128
129
  /**
130
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
131
   * @param {string} bitDepthCode The new bit depth of the samples.
132
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
133
   *    Optional. Default is 16.
134
   */
135
  fromALaw(bitDepthCode='16') {
136
    this.fromScratch(
137
      this.fmt.numChannels,
138
      this.fmt.sampleRate,
139
      '16',
140
      alawmulaw.alaw.decode(this.data.samples),
141
      {container: this.correctContainer_()});
142
    if (bitDepthCode != '16') {
143
      this.toBitDepth(bitDepthCode);
144
    }
145
  }
146
147
  /**
148
   * Encode 16-bit wave file as 8-bit mu-Law.
149
   */
150
  toMuLaw() {
151
    this.assure16Bit_();
152
    /** @type {!Int16Array} */
153
    let output = new Int16Array(this.data.samples.length / 2);
154
    unpackArrayTo(this.data.samples, this.dataType, output);
155
    this.fromScratch(
156
      this.fmt.numChannels,
157
      this.fmt.sampleRate,
158
      '8m',
159
      alawmulaw.mulaw.encode(output),
160
      {container: this.correctContainer_()});
161
  }
162
163
  /**
164
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
165
   * @param {string} bitDepthCode The new bit depth of the samples.
166
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
167
   *    Optional. Default is 16.
168
   */
169
  fromMuLaw(bitDepthCode='16') {
170
    this.fromScratch(
171
      this.fmt.numChannels,
172
      this.fmt.sampleRate,
173
      '16',
174
      alawmulaw.mulaw.decode(this.data.samples),
175
      {container: this.correctContainer_()});
176
    if (bitDepthCode != '16') {
177
      this.toBitDepth(bitDepthCode);
178
    }
179
  }
180
181
  /**
182
   * Return the sample at a given index.
183
   * @param {number} index The sample index.
184
   * @return {number} The sample.
185
   * @throws {Error} If the sample index is off range.
186
   */
187
  getSample(index) {
188
    index = index * (this.dataType.bits / 8);
189
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
190
      throw new Error('Range error');
191
    }
192
    return unpack(
193
      this.data.samples.slice(index, index + this.dataType.bits / 8),
194
      this.dataType);
195
  }
196
197
  /**
198
   * Set the sample at a given index.
199
   * @param {number} index The sample index.
200
   * @param {number} sample The sample.
201
   * @throws {Error} If the sample index is off range.
202
   */
203
  setSample(index, sample) {
204
    index = index * (this.dataType.bits / 8);
205
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
206
      throw new Error('Range error');
207
    }
208
    packTo(sample, this.dataType, this.data.samples, index);
209
  }
210
211
  /**
212
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
213
   * @param {string} base64String A .wav file as a base64 string.
214
   * @throws {Error} If any property of the object appears invalid.
215
   */
216
  fromBase64(base64String) {
217
    this.fromBuffer(new Uint8Array(decode(base64String)));
218
  }
219
220
  /**
221
   * Return a base64 string representig the WaveFile object as a .wav file.
222
   * @return {string} A .wav file as a base64 string.
223
   * @throws {Error} If any property of the object appears invalid.
224
   */
225
  toBase64() {
226
    /** @type {!Uint8Array} */
227
    let buffer = this.toBuffer();
228
    return encode(buffer, 0, buffer.length);
229
  }
230
231
  /**
232
   * Return a DataURI string representig the WaveFile object as a .wav file.
233
   * The return of this method can be used to load the audio in browsers.
234
   * @return {string} A .wav file as a DataURI.
235
   * @throws {Error} If any property of the object appears invalid.
236
   */
237
  toDataURI() {
238
    return 'data:audio/wav;base64,' + this.toBase64();
239
  }
240
241
  /**
242
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
243
   * @param {string} dataURI A .wav file as DataURI.
244
   * @throws {Error} If any property of the object appears invalid.
245
   */
246
  fromDataURI(dataURI) {
247
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
248
  }
249
250
  /**
251
   * Change the bit depth of the samples.
252
   * @param {string} newBitDepth The new bit depth of the samples.
253
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
254
   * @param {boolean} changeResolution A boolean indicating if the
255
   *    resolution of samples should be actually changed or not.
256
   * @throws {Error} If the bit depth is not valid.
257
   */
258
  toBitDepth(newBitDepth, changeResolution=true) {
259
    /** @type {string} */
260
    let toBitDepth = newBitDepth;
261
    /** @type {string} */
262
    let thisBitDepth = this.bitDepth;
263
    if (!changeResolution) {
264
      if (newBitDepth != '32f') {
265
        toBitDepth = this.dataType.bits.toString();
266
      }
267
      thisBitDepth = this.dataType.bits;
268
    }
269
    this.assureUncompressed_();
270
    /** @type {number} */
271
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
272
    /** @type {!Float64Array} */
273
    let typedSamplesInput = new Float64Array(sampleCount);
274
    /** @type {!Float64Array} */
275
    let typedSamplesOutput = new Float64Array(sampleCount);
276
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
277
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
278
      truncateSamples(typedSamplesInput);
279
    }
280
    bitDepthLib(
281
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
282
    this.fromScratch(
283
      this.fmt.numChannels,
284
      this.fmt.sampleRate,
285
      newBitDepth,
286
      typedSamplesOutput,
287
      {container: this.correctContainer_()});
288
  }
289
290
  /**
291
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
292
   * then it is created. It if exists, it is overwritten.
293
   * @param {string} tag The tag name.
294
   * @param {string} value The tag value.
295
   * @throws {Error} If the tag name is not valid.
296
   */
297
  setTag(tag, value) {
298
    tag = fixRIFFTag(tag);
299
    /** @type {!Object} */
300
    let index = this.getTagIndex_(tag);
301
    if (index.TAG !== null) {
302
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
303
        value.length + 1;
304
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
305
    } else if (index.LIST !== null) {
306
      this.LIST[index.LIST].subChunks.push({
307
        chunkId: tag,
308
        chunkSize: value.length + 1,
309
        value: value});
310
    } else {
311
      this.LIST.push({
312
        chunkId: 'LIST',
313
        chunkSize: 8 + value.length + 1,
314
        format: 'INFO',
315
        subChunks: []});
316
      this.LIST[this.LIST.length - 1].subChunks.push({
317
        chunkId: tag,
318
        chunkSize: value.length + 1,
319
        value: value});
320
    }
321
  }
322
323
  /**
324
   * Return the value of a RIFF tag in the INFO chunk.
325
   * @param {string} tag The tag name.
326
   * @return {?string} The value if the tag is found, null otherwise.
327
   */
328
  getTag(tag) {
329
    /** @type {!Object} */
330
    let index = this.getTagIndex_(tag);
331
    if (index.TAG !== null) {
332
      return this.LIST[index.LIST].subChunks[index.TAG].value;
333
    }
334
    return null;
335
  }
336
337
  /**
338
   * Return a Object<tag, value> with the RIFF tags in the file.
339
   * @return {!Object<string, string>} The file tags.
340
   */
341
  listTags() {
342
    /** @type {?number} */
343
    let index = this.getLISTINFOIndex_();
344
    /** @type {!Object} */
345
    let tags = {};
346
    if (index !== null) {
347
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
348
        tags[this.LIST[index].subChunks[i].chunkId] =
349
          this.LIST[index].subChunks[i].value;
350
      }
351
    }
352
    return tags;
353
  }
354
355
  /**
356
   * Remove a RIFF tag from the INFO chunk.
357
   * @param {string} tag The tag name.
358
   * @return {boolean} True if a tag was deleted.
359
   */
360
  deleteTag(tag) {
361
    /** @type {!Object} */
362
    let index = this.getTagIndex_(tag);
363
    if (index.TAG !== null) {
364
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
365
      return true;
366
    }
367
    return false;
368
  }
369
370
  /**
371
   * Create a cue point in the wave file.
372
   * @param {number} position The cue point position in milliseconds.
373
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
374
   */
375
  setCuePoint(position, labl='') {
376
    this.cue.chunkId = 'cue ';
377
    position = (position * this.fmt.sampleRate) / 1000;
378
    /** @type {!Array<!Object>} */
379
    let existingPoints = this.getCuePoints_();
380
    this.clearLISTadtl_();
381
    /** @type {number} */
382
    let len = this.cue.points.length;
383
    this.cue.points = [];
384
    /** @type {boolean} */
385
    let hasSet = false;
386
    if (len === 0) {
387
      this.setCuePoint_(position, 1, labl);
388
    } else {
389
      for (let i = 0; i < len; i++) {
390
        if (existingPoints[i].dwPosition > position && !hasSet) {
391
          this.setCuePoint_(position, i + 1, labl);
392
          this.setCuePoint_(
393
            existingPoints[i].dwPosition,
394
            i + 2,
395
            existingPoints[i].label);
396
          hasSet = true;
397
        } else {
398
          this.setCuePoint_(
399
            existingPoints[i].dwPosition,
400
            i + 1,
401
            existingPoints[i].label);
402
        }
403
      }
404
      if (!hasSet) {
405
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
406
      }
407
    }
408
    this.cue.dwCuePoints = this.cue.points.length;
409
  }
410
411
  /**
412
   * Remove a cue point from a wave file.
413
   * @param {number} index the index of the point. First is 1,
414
   *    second is 2, and so on.
415
   */
416
  deleteCuePoint(index) {
417
    this.cue.chunkId = 'cue ';
418
    /** @type {!Array<!Object>} */
419
    let existingPoints = this.getCuePoints_();
420
    this.clearLISTadtl_();
421
    /** @type {number} */
422
    let len = this.cue.points.length;
423
    this.cue.points = [];
424
    for (let i = 0; i < len; i++) {
425
      if (i + 1 !== index) {
426
        this.setCuePoint_(
427
          existingPoints[i].dwPosition,
428
          i + 1,
429
          existingPoints[i].label);
430
      }
431
    }
432
    this.cue.dwCuePoints = this.cue.points.length;
433
    if (this.cue.dwCuePoints) {
434
      this.cue.chunkId = 'cue ';
435
    } else {
436
      this.cue.chunkId = '';
437
      this.clearLISTadtl_();
438
    }
439
  }
440
441
  /**
442
   * Return an array with all cue points in the file, in the order they appear
443
   * in the file.
444
   * The difference between this method and using the list in WaveFile.cue
445
   * is that the return value of this method includes the position in
446
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
447
   * @return {!Array<!Object>}
448
   */
449
  listCuePoints() {
450
    /** @type {!Array<!Object>} */
451
    let points = this.getCuePoints_();
452
    for (let i = 0, len = points.length; i < len; i++) {
453
      points[i].milliseconds =
454
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
455
    }
456
    return points;
457
  }
458
459
  /**
460
   * Update the label of a cue point.
461
   * @param {number} pointIndex The ID of the cue point.
462
   * @param {string} label The new text for the label.
463
   */
464
  updateLabel(pointIndex, label) {
465
    /** @type {?number} */
466
    let cIndex = this.getAdtlChunk_();
467
    if (cIndex !== null) {
468
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
469
        if (this.LIST[cIndex].subChunks[i].dwName ==
470
            pointIndex) {
471
          this.LIST[cIndex].subChunks[i].value = label;
472
        }
473
      }
474
    }
475
  }
476
477
  /**
478
   * Make the file 16-bit if it is not.
479
   * @private
480
   */
481
  assure16Bit_() {
482
    this.assureUncompressed_();
483
    if (this.bitDepth != '16') {
484
      this.toBitDepth('16');
485
    }
486
  }
487
488
  /**
489
   * Uncompress the samples in case of a compressed file.
490
   * @private
491
   */
492
  assureUncompressed_() {
493
    if (this.bitDepth == '8a') {
494
      this.fromALaw();
495
    } else if (this.bitDepth == '8m') {
496
      this.fromMuLaw();
497
    } else if (this.bitDepth == '4') {
498
      this.fromIMAADPCM();
499
    }
500
  }
501
  
502
  /**
503
   * Push a new cue point in this.cue.points.
504
   * @param {number} position The position in milliseconds.
505
   * @param {number} dwName the dwName of the cue point
506
   * @private
507
   */
508
  setCuePoint_(position, dwName, label) {
509
    this.cue.points.push({
510
      dwName: dwName,
511
      dwPosition: position,
512
      fccChunk: 'data',
513
      dwChunkStart: 0,
514
      dwBlockStart: 0,
515
      dwSampleOffset: position,
516
    });
517
    this.setLabl_(dwName, label);
518
  }
519
520
  /**
521
   * Return an array with all cue points in the file, in the order they appear
522
   * in the file.
523
   * @return {!Array<!Object>}
524
   * @private
525
   */
526
  getCuePoints_() {
527
    /** @type {!Array<!Object>} */
528
    let points = [];
529
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
530
      points.push({
531
        dwPosition: this.cue.points[i].dwPosition,
532
        label: this.getLabelForCuePoint_(
533
          this.cue.points[i].dwName)});
534
    }
535
    return points;
536
  }
537
538
  /**
539
   * Return the label of a cue point.
540
   * @param {number} pointDwName The ID of the cue point.
541
   * @return {string}
542
   * @private
543
   */
544
  getLabelForCuePoint_(pointDwName) {
545
    /** @type {?number} */
546
    let cIndex = this.getAdtlChunk_();
547
    if (cIndex !== null) {
548
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
549
        if (this.LIST[cIndex].subChunks[i].dwName ==
550
            pointDwName) {
551
          return this.LIST[cIndex].subChunks[i].value;
552
        }
553
      }
554
    }
555
    return '';
556
  }
557
558
  /**
559
   * Clear any LIST chunk labeled as 'adtl'.
560
   * @private
561
   */
562
  clearLISTadtl_() {
563
    for (let i = 0, len = this.LIST.length; i < len; i++) {
564
      if (this.LIST[i].format == 'adtl') {
565
        this.LIST.splice(i);
566
      }
567
    }
568
  }
569
570
  /**
571
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
572
   * @param {number} dwName The ID of the cue point.
573
   * @param {string} label The label for the cue point.
574
   * @private
575
   */
576
  setLabl_(dwName, label) {
577
    /** @type {?number} */
578
    let adtlIndex = this.getAdtlChunk_();
579
    if (adtlIndex === null) {
580
      this.LIST.push({
581
        chunkId: 'LIST',
582
        chunkSize: 4,
583
        format: 'adtl',
584
        subChunks: []});
585
      adtlIndex = this.LIST.length - 1;
586
    }
587
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
588
  }
589
590
  /**
591
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
592
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
593
   * @param {number} dwName The ID of the cue point.
594
   * @param {string} label The label for the cue point.
595
   * @private
596
   */
597
  setLabelText_(adtlIndex, dwName, label) {
598
    this.LIST[adtlIndex].subChunks.push({
599
      chunkId: 'labl',
600
      chunkSize: label.length,
601
      dwName: dwName,
602
      value: label
603
    });
604
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
605
  }
606
607
  /**
608
   * Return the index of the 'adtl' LIST in this.LIST.
609
   * @return {?number}
610
   * @private
611
   */
612
  getAdtlChunk_() {
613
    for (let i = 0, len = this.LIST.length; i < len; i++) {
614
      if (this.LIST[i].format == 'adtl') {
615
        return i;
616
      }
617
    }
618
    return null;
619
  }
620
621
  /**
622
   * Return the index of the INFO chunk in the LIST chunk.
623
   * @return {?number} the index of the INFO chunk.
624
   * @private
625
   */
626
  getLISTINFOIndex_() {
627
    /** @type {?number} */
628
    let index = null;
629
    for (let i = 0, len = this.LIST.length; i < len; i++) {
630
      if (this.LIST[i].format === 'INFO') {
631
        index = i;
632
        break;
633
      }
634
    }
635
    return index;
636
  }
637
638
  /**
639
   * Return the index of a tag in a FILE chunk.
640
   * @param {string} tag The tag name.
641
   * @return {!Object<string, ?number>}
642
   *    Object.LIST is the INFO index in LIST
643
   *    Object.TAG is the tag index in the INFO
644
   * @private
645
   */
646
  getTagIndex_(tag) {
647
    /** @type {!Object<string, ?number>} */
648
    let index = {LIST: null, TAG: null};
649
    for (let i = 0, len = this.LIST.length; i < len; i++) {
650
      if (this.LIST[i].format == 'INFO') {
651
        index.LIST = i;
652
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
653
          if (this.LIST[i].subChunks[j].chunkId == tag) {
654
            index.TAG = j;
655
            break;
656
          }
657
        }
658
        break;
659
      }
660
    }
661
    return index;
662
  }
663
664
  /**
665
   * Return 'RIFF' if the container is 'RF64', the current container name
666
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
667
   * @return {string}
668
   * @private
669
   */
670
  correctContainer_() {
671
    return this.container == 'RF64' ? 'RIFF' : this.container;
672
  }
673
}
674